/*
 * Copyright 2017 Matthew Tamlin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.matthewtamlin.fortytwo.library.answer_view;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorGroup;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.LayoutScatter;
import ohos.agp.components.StackLayout;
import ohos.agp.components.Text;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.utils.Color;
import ohos.app.Context;

import com.matthewtamlin.fortytwo.library.ResourceTable;
import com.matthewtamlin.fortytwo.library.answer.Answer;
import com.matthewtamlin.fortytwo.library.util.Utils;
import com.matthewtamlin.java_utilities.checkers.IntChecker;

/**
 * A implementation of the AnswerView interface which uses a CardView for the UI. Although all
 * interface methods are implemented, the class is declared abstract because the UI is never updated
 * to reflect the selected status and the marked status. Despite this, all getters work as expected.
 * The accessibility content description of the view is automatically set based on the current
 * status and answer, however custom content descriptions can be set by passing false to {@link
 * #enableAutomaticContentDescriptions(boolean)} and setting the content description as usual.
 */
public abstract class SimpleAnswerCard extends StackLayout implements AnswerView {
    /**
     * The main UI component, containing the answer container and identifier container.
     */
    private StackLayout card;

    /**
     * Displays the answer to the user.
     */
    private Text answerContainer;

    /**
     * Displays the identifier to the user.
     */
    private Text identifierContainer;

    /**
     * Indicates whether or not the containers are currently being updated. This could be an
     * instantaneous update on the UI thread, or an asynchronous update using animators.
     */
    private boolean textUpdateInProgress = false;

    /**
     * Indicates whether or not an update needs to be performed when possible.
     */
    private boolean textUpdatePending = false;

    /**
     * Indicates whether or not the next update should be animated or instantaneous.
     */
    private boolean animateNextTextUpdate = false;

    /**
     * The duration to use for animated updates, measured in milliseconds.
     */
    private int animationDurationMs = 300;

    /**
     * Whether or not this card is currently marked. This value is not reflected in the UI, as this
     * responsibility is delegated to subclasses.
     */
    private boolean marked = false;

    /**
     * Whether or not this card is currently selected. This value is not reflected in the UI, as
     * this responsibility is delegated to subclasses.
     */
    private boolean selected = false;

    /**
     * The current answer.
     */
    private Answer answer = null;

    /**
     * The current identifier.
     */
    private CharSequence identifier = "";

    /**
     * Whether or not the content description should automatically be updated to reflect the status
     * and answer of the view.
     */
    private boolean enableAutomaticContentDescriptions = true;

    /**
     * Constructs a new SimpleAnswerCard. The marked and selected statuses are both set to false by
     * default.
     *
     * @param context the context this view is operating in, not null
     */
    public SimpleAnswerCard(final Context context) {
        super(context);
        init();
    }

    /**
     * Constructs a new SimpleAnswerCard. The marked and selected statuses are both set to false by
     * default.
     *
     * @param context the context this view is operating in, not null
     * @param attrs configuration attributes, null allowed
     */
    public SimpleAnswerCard(final Context context, final AttrSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * Constructs a new SimpleAnswerCard. The marked and selected statuses are both set to false by
     * default.
     *
     * @param context the context this view is operating in, not null
     * @param attrs configuration attributes, null allowed
     * @param defStyleAttr an attribute in the current theme which supplies default attributes, pass 0	to ignore
     */
    public SimpleAnswerCard(final Context context, final AttrSet attrs,
                            final String defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * getCard
     *
     * @return the CardView used for the main body of this view
     */
    public StackLayout getCard() {
        return card;
    }

    /**
     * getAnswerContainer
     *
     * @return the TextView used to display the answer
     */
    public Text getAnswerContainer() {
        return answerContainer;
    }

    /**
     * getIdentifierContainer
     *
     * @return the TextView used to display the identifier
     */
    public Text getIdentifierContainer() {
        return identifierContainer;
    }

    /**
     * Sets the animation duration to use when updating the UI. The default is 300 milliseconds.
     *
     * @param animationDurationMs the duration to use, measured in milliseconds, at least 0
     */
    public void setAnimationDurationMs(final int animationDurationMs) {
        this.animationDurationMs = IntChecker.checkGreaterThanOrEqualTo(animationDurationMs, 0,
            "animationDurationMs cannot be less than zero.");
    }

    /**
     * getAnimationDurationMs
     *
     * @return the current animation duration, measured in milliseconds
     */
    public int getAnimationDurationMs() {
        return animationDurationMs;
    }

    /**
     * Enables/disables automatic content descriptions. If automatic descriptions are enabled, the
     * content description is automatically set to reflect the current status and answer. If this
     * option is disabled, the content description can be set externally.
     *
     * @param enable true to enable automatic content descriptions, false to disable them
     */
    public void enableAutomaticContentDescriptions(final boolean enable) {
        enableAutomaticContentDescriptions = enable;

        // May have been false previously, so ensure content descriptions reflects new setting
        if (enable) {
            updateAccessibility();
        }
    }

    /**
     * Returns whether or not the current answer is correct. If there is currently no answer, false
     * is returned.
     *
     * @return boolean
     */
    public boolean answerIsCorrect() {
        return answer == null ? false : answer.isCorrect();
    }

    @Override
    public void setStatus(final boolean marked, final boolean selected, final boolean animate) {
        this.marked = marked;
        this.selected = selected;

        updateAccessibility();
    }

    @Override
    public void setMarkedStatus(final boolean marked, final boolean animate) {
        setStatus(marked, isSelected(), animate);
    }

    @Override
    public void setSelectedStatus(final boolean selected, final boolean animate) {
        setStatus(isMarked(), selected, animate);
    }

    @Override
    public boolean isMarked() {
        return marked;
    }

    @Override
    public boolean isSelected() {
        return selected;
    }

    @Override
    public void setAnswer(final Answer answer, final boolean animate) {
        this.answer = answer;

        updateAccessibility();
        updateText(animate);
    }

    @Override
    public Answer getAnswer() {
        return answer;
    }

    @Override
    public void setIdentifier(final CharSequence identifier, final boolean animate) {
        this.identifier = identifier;

        updateAccessibility();
        updateText(animate);
    }

    @Override
    public CharSequence getIdentifier() {
        return identifier;
    }

    /**
     * Initialises this view. The UI is inflated and default values are displayed.
     */
    private void init() {
        LayoutScatter.getInstance(getContext()).parse(ResourceTable.Layout_single_answer_card, this, true);

        card = (StackLayout) findComponentById(ResourceTable.Id_single_answer_card_root);
        identifierContainer = (Text) findComponentById(ResourceTable.Id_single_answer_card_identifier);
        answerContainer = (Text) findComponentById(ResourceTable.Id_single_answer_card_label);

        int backgroundColor = ((ShapeElement) card.getBackgroundElement().getCurrentElement())
            .getRgbColors()[0].asArgbInt();
        if (backgroundColor == 0) {
            backgroundColor = -1;
        }
        Color color = new Color(Utils.calculateBestTextColor(backgroundColor));
        identifierContainer.setTextColor(color);
        answerContainer.setTextColor(color);
        updateAccessibility();
        updateText(false);
    }

    /**
     * Updates the accessibility properties of this view.
     */
    private void updateAccessibility() {
        if (enableAutomaticContentDescriptions) {
            if (answer == null) {
                setComponentDescription(getContext().getString(ResourceTable.String_single_answer_view_contdesc_desc_blank));
            } else {
                final String selectedText = getContext().getString(ResourceTable.String_single_answer_view_contdesc_general_selected);
                final String unselectedText = getContext().getString(ResourceTable.String_single_answer_view_contdesc_general_unselected);
                final String markedCorrectText = getContext().getString(ResourceTable.String_single_answer_view_contdesc_general_marked_correct);
                final String markedIncorrectText = getContext().getString(ResourceTable.String_single_answer_view_contdesc_general_marked_incorrect);
                final String unmarkedText = getContext().getString(ResourceTable.String_single_answer_view_contdesc_general_unmarked);

                final String selectedVariable = selected ? selectedText : unselectedText;
                final String markedVariable = marked
                    ? (answer.isCorrect() ? markedCorrectText : markedIncorrectText)
                    : unmarkedText;

                setComponentDescription(String.format(getContext().getString(
                    ResourceTable.String_single_answer_view_contdesc_general),
                    selectedVariable,
                    markedVariable));
            }
        }
    }

    /**
     * Updates the UI to display the current answer and identifier. If this method is called again
     * while animations from a previous invocation are still running, the current animations will
     * complete before new ones are started.
     *
     * @param animate whether or not the UI update should be animated
     */
    private void updateText(final boolean animate) {
        textUpdatePending = true;
        animateNextTextUpdate = animate;

        if (!textUpdateInProgress) {
            textUpdateInProgress = true;
            textUpdatePending = false;

            final CharSequence answerText = answer == null ? "" : answer.getText();
            final boolean updateAnswer = !answerContainer.getText().equals(answerText);
            final boolean updateIdentifier = !identifierContainer.getText().equals(identifier);

            if (!animateNextTextUpdate || animationDurationMs == 0) {
                // Reset then set new values
                answerContainer.setText(null);
                identifierContainer.setText(null);
                answerContainer.setText(answerText.toString());
                identifierContainer.setText(identifier.toString());

                textUpdateInProgress = false;
            } else {
                AnimatorValue fadeOutCurrent = new AnimatorValue();
                fadeOutCurrent.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
                    @Override
                    public void onUpdate(AnimatorValue animatorValue, float v) {
                        if (updateAnswer) {
                            answerContainer.setAlpha(1 - v);
                        }

                        if (updateIdentifier) {
                            identifierContainer.setAlpha(1 - v);
                        }
                    }
                });

                AnimatorValue fadeInNew = new AnimatorValue();
                fadeInNew.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
                    @Override
                    public void onUpdate(AnimatorValue animatorValue, float v) {
                        if (updateAnswer) {
                            answerContainer.setAlpha(v);
                        }

                        if (updateIdentifier) {
                            identifierContainer.setAlpha(v);
                        }
                    }
                });
                fadeInNew.setStateChangedListener(new Animator.StateChangedListener() {
                    @Override
                    public void onStart(Animator animator) {
                        // Reset then set new values
                        answerContainer.setText(null);
                        identifierContainer.setText(null);
                        answerContainer.setText(answerText.toString());
                        identifierContainer.setText(identifier.toString());
                    }

                    @Override
                    public void onStop(Animator animator) {
                    }

                    @Override
                    public void onCancel(Animator animator) {
                    }

                    @Override
                    public void onEnd(Animator animator) {
                        textUpdateInProgress = false;

                        // If a new update was requested while the animation progressed, do it now
                        if (textUpdatePending) {
                            updateText(animateNextTextUpdate);
                        }
                    }

                    @Override
                    public void onPause(Animator animator) {
                    }

                    @Override
                    public void onResume(Animator animator) {
                    }
                });

                AnimatorGroup group = new AnimatorGroup();
                group.setDuration(animationDurationMs / 2);
                group.runSerially(fadeOutCurrent, fadeInNew);
                group.start();
            }
        }
    }
}